Etant un amateur de basket-ball mais aussi un amateur de machine-learning, je vous propose de vous présenter les travaux que j’ai pu réaliser pour la compétition kaggle : Kobe Bryant Shot Selection. Le but de cette compétition est de prédire la probabilité de réussite aux tirs de Kobe Bryant. Pour ce faire, nous disposons de l’ensemble de ses shots de sa carrière. Ces derniers sont représentés suivant un grand nombre de variables que nous verrons par la suite. Seule une variable est importante à ce stage : ‘shot_made_flag’ qui nous dit si le shot a été réussi ou non. Elle a pour valeur 0 ou 1 et représente une probabilité. Pour mettre en place la compétition, kaggle a retiré l’information ‘shot_made_flag’ de 5000 tirs au hasard. A nous d’estimer cette probabilité.

Chargement des données

shots = read.csv("data/data.csv", stringsAsFactors = T)

shots$shot_made_flag <- as.factor(shots$shot_made_flag)

train = shots[!is.na(shots$shot_made_flag),]
test = shots[is.na(shots$shot_made_flag),]

Une fois le chargement des données effectuées dans la variable shots, on peut distinguer deux jeux de données : - train : les shots où l’on connaît la réussite du tir, - test : les shots où l’on ne connaît pas la réussite du tir.

Prise de connaissance

names(shots)
##  [1] "action_type"        "combined_shot_type" "game_event_id"     
##  [4] "game_id"            "lat"                "loc_x"             
##  [7] "loc_y"              "lon"                "minutes_remaining" 
## [10] "period"             "playoffs"           "season"            
## [13] "seconds_remaining"  "shot_distance"      "shot_made_flag"    
## [16] "shot_type"          "shot_zone_area"     "shot_zone_basic"   
## [19] "shot_zone_range"    "team_id"            "team_name"         
## [22] "game_date"          "matchup"            "opponent"          
## [25] "shot_id"
summary(shots)
##                action_type    combined_shot_type game_event_id  
##  Jump Shot           :18880   Bank Shot:  141    Min.   :  2.0  
##  Layup Shot          : 2567   Dunk     : 1286    1st Qu.:110.0  
##  Driving Layup Shot  : 1978   Hook Shot:  153    Median :253.0  
##  Turnaround Jump Shot: 1057   Jump Shot:23485    Mean   :249.2  
##  Fadeaway Jump Shot  : 1048   Layup    : 5448    3rd Qu.:368.0  
##  Running Jump Shot   :  926   Tip Shot :  184    Max.   :659.0  
##  (Other)             : 4241                                     
##     game_id              lat            loc_x              loc_y       
##  Min.   :20000012   Min.   :33.25   Min.   :-250.000   Min.   :-44.00  
##  1st Qu.:20500077   1st Qu.:33.88   1st Qu.: -68.000   1st Qu.:  4.00  
##  Median :20900354   Median :33.97   Median :   0.000   Median : 74.00  
##  Mean   :24764066   Mean   :33.95   Mean   :   7.111   Mean   : 91.11  
##  3rd Qu.:29600474   3rd Qu.:34.04   3rd Qu.:  95.000   3rd Qu.:160.00  
##  Max.   :49900088   Max.   :34.09   Max.   : 248.000   Max.   :791.00  
##                                                                        
##       lon         minutes_remaining     period         playoffs     
##  Min.   :-118.5   Min.   : 0.000    Min.   :1.000   Min.   :0.0000  
##  1st Qu.:-118.3   1st Qu.: 2.000    1st Qu.:1.000   1st Qu.:0.0000  
##  Median :-118.3   Median : 5.000    Median :3.000   Median :0.0000  
##  Mean   :-118.3   Mean   : 4.886    Mean   :2.519   Mean   :0.1466  
##  3rd Qu.:-118.2   3rd Qu.: 8.000    3rd Qu.:3.000   3rd Qu.:0.0000  
##  Max.   :-118.0   Max.   :11.000    Max.   :7.000   Max.   :1.0000  
##                                                                     
##      season      seconds_remaining shot_distance   shot_made_flag
##  2005-06: 2318   Min.   : 0.00     Min.   : 0.00   0   :14232    
##  2008-09: 2242   1st Qu.:13.00     1st Qu.: 5.00   1   :11465    
##  2002-03: 2241   Median :28.00     Median :15.00   NA's: 5000    
##  2007-08: 2153   Mean   :28.37     Mean   :13.44                 
##  2009-10: 2080   3rd Qu.:43.00     3rd Qu.:21.00                 
##  2001-02: 2028   Max.   :59.00     Max.   :79.00                 
##  (Other):17635                                                   
##           shot_type                   shot_zone_area 
##  2PT Field Goal:24271   Back Court(BC)       :   83  
##  3PT Field Goal: 6426   Center(C)            :13455  
##                         Left Side Center(LC) : 4044  
##                         Left Side(L)         : 3751  
##                         Right Side Center(RC): 4776  
##                         Right Side(R)        : 4588  
##                                                      
##               shot_zone_basic         shot_zone_range    team_id         
##  Above the Break 3    : 5620   16-24 ft.      :8315   Min.   :1.611e+09  
##  Backcourt            :   71   24+ ft.        :6275   1st Qu.:1.611e+09  
##  In The Paint (Non-RA): 4578   8-16 ft.       :6626   Median :1.611e+09  
##  Left Corner 3        :  280   Back Court Shot:  83   Mean   :1.611e+09  
##  Mid-Range            :12625   Less Than 8 ft.:9398   3rd Qu.:1.611e+09  
##  Restricted Area      : 7136                          Max.   :1.611e+09  
##  Right Corner 3       :  387                                             
##               team_name          game_date            matchup     
##  Los Angeles Lakers:30697   2016-04-13:   50   LAL @ SAS  : 1020  
##                             2002-11-07:   47   LAL vs. SAS:  936  
##                             2006-01-22:   46   LAL @ SAC  :  889  
##                             2006-12-29:   45   LAL vs. HOU:  878  
##                             2007-03-30:   44   LAL @ DEN  :  873  
##                             2008-01-14:   44   LAL @ PHX  :  859  
##                             (Other)   :30421   (Other)    :25242  
##     opponent        shot_id     
##  SAS    : 1978   Min.   :    1  
##  PHX    : 1781   1st Qu.: 7675  
##  HOU    : 1666   Median :15349  
##  SAC    : 1643   Mean   :15349  
##  DEN    : 1642   3rd Qu.:23023  
##  POR    : 1539   Max.   :30697  
##  (Other):20448
str(shots)
## 'data.frame':    30697 obs. of  25 variables:
##  $ action_type       : Factor w/ 57 levels "Alley Oop Dunk Shot",..: 27 27 27 27 6 27 28 27 27 42 ...
##  $ combined_shot_type: Factor w/ 6 levels "Bank Shot","Dunk",..: 4 4 4 4 2 4 5 4 4 4 ...
##  $ game_event_id     : int  10 12 35 43 155 244 251 254 265 294 ...
##  $ game_id           : int  20000012 20000012 20000012 20000012 20000012 20000012 20000012 20000012 20000012 20000012 ...
##  $ lat               : num  34 34 33.9 33.9 34 ...
##  $ loc_x             : int  167 -157 -101 138 0 -145 0 1 -65 -33 ...
##  $ loc_y             : int  72 0 135 175 0 -11 0 28 108 125 ...
##  $ lon               : num  -118 -118 -118 -118 -118 ...
##  $ minutes_remaining : int  10 10 7 6 6 9 8 8 6 3 ...
##  $ period            : int  1 1 1 1 2 3 3 3 3 3 ...
##  $ playoffs          : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ season            : Factor w/ 20 levels "1996-97","1997-98",..: 5 5 5 5 5 5 5 5 5 5 ...
##  $ seconds_remaining : int  27 22 45 52 19 32 52 5 12 36 ...
##  $ shot_distance     : int  18 15 16 22 0 14 0 2 12 12 ...
##  $ shot_made_flag    : Factor w/ 2 levels "0","1": NA 1 2 1 2 1 2 NA 2 1 ...
##  $ shot_type         : Factor w/ 2 levels "2PT Field Goal",..: 1 1 1 1 1 1 1 1 1 1 ...
##  $ shot_zone_area    : Factor w/ 6 levels "Back Court(BC)",..: 6 4 3 5 2 4 2 2 4 2 ...
##  $ shot_zone_basic   : Factor w/ 7 levels "Above the Break 3",..: 5 5 5 5 6 5 6 6 3 3 ...
##  $ shot_zone_range   : Factor w/ 5 levels "16-24 ft.","24+ ft.",..: 1 3 1 1 5 3 5 5 3 3 ...
##  $ team_id           : int  1610612747 1610612747 1610612747 1610612747 1610612747 1610612747 1610612747 1610612747 1610612747 1610612747 ...
##  $ team_name         : Factor w/ 1 level "Los Angeles Lakers": 1 1 1 1 1 1 1 1 1 1 ...
##  $ game_date         : Factor w/ 1559 levels "1996-11-03","1996-11-05",..: 311 311 311 311 311 311 311 311 311 311 ...
##  $ matchup           : Factor w/ 74 levels "LAL @ ATL","LAL @ BKN",..: 29 29 29 29 29 29 29 29 29 29 ...
##  $ opponent          : Factor w/ 33 levels "ATL","BKN","BOS",..: 26 26 26 26 26 26 26 26 26 26 ...
##  $ shot_id           : int  1 2 3 4 5 6 7 8 9 10 ...

Ces fonctions nous permettent de mieux connaitre notre jeu de données. On voit que l’on a la position du tir sur le terrain, s’il s’agit d’un tir à 2 points, à 3 points, l’équipe adverse, le type de tir, etc. Nous reviendrons sur certaines de ces variables plus tard. Une chose remarquable à noter pour une étude statistiques est que il n’y a pas de donnée manquante.

Pour mieux appréhender nos données, une bonne solution est de les visualiser. Pour ce faire, nous définissons une fonction qui permet de représenter une variable en fonction de la position du tir sur le terrain.

courtplot <- function(feat) {
    feat <- substitute(feat)
    train %>% 
    ggplot(aes(x = lon, y = lat)) +
        geom_point(aes_q(color = feat), alpha = 0.7, size = 3) +
        ylim(c(33.7, 34.0883)) +
        theme_void() +
        ggtitle(paste(feat))
}

Est-ce qu’il existe un lien fort entre la position du tir et sa réussite ? Voyons ça :

courtplot(shot_made_flag)
## Warning: Removed 105 rows containing missing values (geom_point).

Nous obtenons une jolie représentation des tirs! On voit que Kobe Bryant a pris des tirs à peu prés partout sur un demi-terrain. On peut également voir une proéminence de la couleur rouge, qui indique un tir manqué. On peut noté aussi que Kobe a pris quelques “big deeper” et qu’il a y eu plus d’échec que de réussite.

Exploration des données

La zone de tir

On dispose d’une variable zone_shot_area. On peut aisément la représenter :

courtplot(shot_zone_area)
## Warning: Removed 105 rows containing missing values (geom_point).

On voit que nous n’avons que 6 zones, ce qui est peu. Cinq zones englobent des tirs à 2 et 3 points et la zone “Back Court” n’apparait pas sur le graphique. Les tirs’Back court’ sont les tirs pris dans la zone adverse.

prop.table(table(train$shot_made_flag, train$shot_zone_area), 2)
##    
##     Back Court(BC)  Center(C) Left Side Center(LC) Left Side(L)
##   0     0.98611111 0.47444415           0.63882283   0.60312899
##   1     0.01388889 0.52555585           0.36117717   0.39687101
##    
##     Right Side Center(RC) Right Side(R)
##   0            0.61743281    0.59834154
##   1            0.38256719    0.40165846

98% de manqués pour les tirs back court!

Il existe aussi une variable shot_zone_basic :

courtplot(shot_zone_basic)
## Warning: Removed 105 rows containing missing values (geom_point).

On voit le même problème que précédemment, la zone ‘Backcourt’ n’est pas représentée et certaines zones sont très larges.

prop.table(table(train$shot_made_flag, train$shot_zone_basic), 2)
##    
##     Above the Break 3  Backcourt In The Paint (Non-RA) Left Corner 3
##   0        0.67076271 0.98333333            0.54561856    0.62916667
##   1        0.32923729 0.01666667            0.45438144    0.37083333
##    
##      Mid-Range Restricted Area Right Corner 3
##   0 0.59371439      0.38199595     0.66066066
##   1 0.40628561      0.61800405     0.33933934

62% de réussite dans la zone ‘Restricted Area’. En fouillant sur le site http://stats.nba.com/, on peut trouver une représentation du terrain découpé en 14 zones.

On peut alors introduire une nouvelle variable pour avoir un découpage du terrain comme sur l’image. En utilisant les positions du tir, si le tir est à 2 ou 3 points, on peut créer les 14 zones de l’image tout en conservant la zone ‘Restricted Area’ et ‘Backcourt’.

train$shot_zone_detailed <- NA
train$shot_zone_detailed[train$loc_x <= -220 & train$loc_y <= 100 & train$shot_type == "3PT Field Goal"] = "A"
train$shot_zone_detailed[train$loc_x >= -220 & train$loc_x <= -150 & train$loc_y <= 100 & train$shot_type == "2PT Field Goal"] = "B"
train$shot_zone_detailed[train$loc_x < -90 & train$shot_type == "3PT Field Goal" & train$loc_y > 100] = "C"
train$shot_zone_detailed[train$loc_x < 90 & train$loc_x > -90 & train$shot_type == "3PT Field Goal"] = "D"

train$shot_zone_detailed[train$loc_x < 70 & train$loc_x > -70 & train$loc_y > 150 & train$shot_type == "2PT Field Goal"] = "F"
train$shot_zone_detailed[train$loc_x > 70 & train$shot_type == "3PT Field Goal"] = "G"

train$shot_zone_detailed[train$loc_x < 90 & train$loc_x > -90 & train$loc_y > 90 & train$loc_y < 150 & train$shot_type == "2PT Field Goal"] = "J"

train$shot_zone_detailed[sqrt(train$loc_x^2 + train$loc_y^2) < 90] = "L"
train$shot_zone_detailed[train$loc_x < 220 & train$loc_x > 150 & train$loc_y <= 100] = "M"
train$shot_zone_detailed[train$loc_x > 220 & train$loc_y < 100 & train$shot_type == "3PT Field Goal"] = "N"

train$shot_zone_detailed[is.na(train$shot_zone_detailed) & train$loc_y > 100 & train$loc_x > -210 & train$loc_x < 70] = "E"
train$shot_zone_detailed[is.na(train$shot_zone_detailed) & train$loc_y > 100 & train$loc_x >= 70 & train$loc_x < 210] = "H"

train$shot_zone_detailed[is.na(train$shot_zone_detailed) & train$loc_y <= 100 & train$loc_x < 0] = "I"
train$shot_zone_detailed[is.na(train$shot_zone_detailed) & train$loc_y <= 100 & train$loc_x > 0] = "K"

train$shot_zone_detailed[train$shot_zone_basic == 'Restricted Area'] = "Restricted Area"
train$shot_zone_detailed[train$shot_zone_basic == 'Backcourt'] = "Backcourt"

On peut ensuite visualiser cette variable

courtplot(shot_zone_detailed)
## Warning: Removed 105 rows containing missing values (geom_point).

Super! Pour le fun, on peut s’intéresser au pourcentage de réussite d’un joueur professionnel sur toute sa carrière. On commence par calculer la moyenne des coordonnées des positions par zone pour pouvoir représenter nos chiffres :

mean_x = aggregate(train$loc_x, list(train$shot_zone_detailed), na.rm = TRUE, mean)
mean_y = aggregate(train$loc_y, list(train$shot_zone_detailed), na.rm = TRUE, mean)
mean_xy = data.frame(mean_x$Group.1, mean_x$x, mean_y$x)

On calcule ensuite le pourcentage de réussite par zone, ainsi que le nombre de tirs par zone :

pourcentage_shot = as.data.frame(prop.table(table(train$shot_made_flag, train$shot_zone_detailed), 2))
shot_made_by_zone = as.data.frame(table(train$shot_made_flag, train$shot_zone_detailed))

On peut ensuite représenter nos pourcentages :

courtimg = readPNG('court.png')
plot(1, type="n", xlab="", ylab="", xlim=c(-235, 235), ylim=c(-30, 400))
lim <- par()
rasterImage(courtimg, lim$usr[1], lim$usr[3], lim$usr[2], lim$usr[4])
grid()
for (zone in c("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "Restricted Area", "Backcourt")) {
  x = mean_xy[mean_xy$mean_x.Group.1 == zone,]$mean_x.x
  y = mean_xy[mean_xy$mean_x.Group.1 == zone,]$mean_y.x
  text_pourcentage = pourcentage_shot[pourcentage_shot$Var1 == 1 & pourcentage_shot$Var2 == zone,]$Freq
  success_shot = shot_made_by_zone[shot_made_by_zone$Var2 == zone & shot_made_by_zone$Var1 == 1,]$Freq
  missed_shot = shot_made_by_zone[shot_made_by_zone$Var2 == zone & shot_made_by_zone$Var1 == 0,]$Freq
  if (zone == "Backcourt") {
    text(0, 390, sprintf("%d%% \n %d / %d", round(text_pourcentage * 100), success_shot, success_shot + missed_shot))
  } else {
    text(x, y, sprintf("%d%% \n %d / %d", round(text_pourcentage * 100), success_shot, success_shot + missed_shot))
  }
}

Le tir à 3 points

Black Mamba était-il un shoteur à 3 points comme Stefen Curry ? Au vue des pourcentages de shot précédents, on peut rapidement estimer un pourcentage de 30% de réussite à 3 pts. Vérifions-ça :

prop.table(table(train$shot_made_flag, train$shot_type))
##    
##     2PT Field Goal 3PT Field Goal
##   0     0.41257734     0.14126163
##   1     0.37681441     0.06934662
prop.table(table(train$shot_made_flag, train$shot_type), 2)
##    
##     2PT Field Goal 3PT Field Goal
##   0      0.5226522      0.6707317
##   1      0.4773478      0.3292683

Pour approximativement 21% de shots pris à 3 points, 33% de réussite! Pour les tirs à 2pts, 52% de manqués contre 47% de succès. Le tir à 2 pts ne nous aidera pas à déterminer la réussite du tir.

La distance

Intéressons nous maintenant à la distance de tir. Nous disposons de deux variables pour ce faire : shot_zone_range, shot_distance.

courtplot(shot_zone_range)
## Warning: Removed 105 rows containing missing values (geom_point).

ggplot(data = train, aes_q(x = train$shot_distance)) +
  geom_bar(aes(fill = shot_made_flag), stat = "count", position = "fill")

On voit clairement que plus le joueur est proche du panier et plus sa réussite est importante.

Le type d’action

On dispose de deux variables pour représenter le type d’action : ‘action_type’ et ‘combined_shot_type’

head(train[,c("action_type", "combined_shot_type")])
##         action_type combined_shot_type
## 2         Jump Shot          Jump Shot
## 3         Jump Shot          Jump Shot
## 4         Jump Shot          Jump Shot
## 5 Driving Dunk Shot               Dunk
## 6         Jump Shot          Jump Shot
## 7        Layup Shot              Layup
summary(train$action_type)
##                Alley Oop Dunk Shot               Alley Oop Layup shot 
##                                 95                                 67 
##     Cutting Finger Roll Layup Shot                 Cutting Layup Shot 
##                                  0                                  6 
##                  Driving Bank shot                  Driving Dunk Shot 
##                                  3                                257 
##     Driving Finger Roll Layup Shot           Driving Finger Roll Shot 
##                                 59                                 68 
##    Driving Floating Bank Jump Shot         Driving Floating Jump Shot 
##                                  1                                  3 
##                  Driving Hook Shot                  Driving Jump shot 
##                                 13                                 23 
##                 Driving Layup Shot         Driving Reverse Layup Shot 
##                               1628                                 83 
##             Driving Slam Dunk Shot                          Dunk Shot 
##                                 43                                217 
##                 Fadeaway Bank shot                 Fadeaway Jump Shot 
##                                 27                                872 
##             Finger Roll Layup Shot                   Finger Roll Shot 
##                                 28                                 26 
##                 Floating Jump shot                Follow Up Dunk Shot 
##                                 93                                 10 
##                     Hook Bank Shot                          Hook Shot 
##                                  5                                 73 
##                     Jump Bank Shot                     Jump Hook Shot 
##                                289                                 19 
##                          Jump Shot                         Layup Shot 
##                              15836                               2154 
##                   Pullup Bank shot                   Pullup Jump shot 
##                                 11                                402 
##                  Putback Dunk Shot                 Putback Layup Shot 
##                                  3                                  9 
##             Putback Slam Dunk Shot                  Reverse Dunk Shot 
##                                  2                                 61 
##                 Reverse Layup Shot             Reverse Slam Dunk Shot 
##                                333                                 15 
##                  Running Bank shot                  Running Dunk Shot 
##                                 43                                 18 
##     Running Finger Roll Layup Shot           Running Finger Roll Shot 
##                                  5                                  4 
##                  Running Hook Shot                  Running Jump Shot 
##                                 33                                779 
##                 Running Layup Shot          Running Pull-Up Jump Shot 
##                                 51                                  3 
##         Running Reverse Layup Shot             Running Slam Dunk Shot 
##                                  7                                  1 
##                   Running Tip Shot                     Slam Dunk Shot 
##                                  1                                334 
##                Step Back Jump shot                     Tip Layup Shot 
##                                106                                  2 
##                           Tip Shot               Turnaround Bank shot 
##                                151                                 58 
## Turnaround Fadeaway Bank Jump Shot           Turnaround Fadeaway shot 
##                                  0                                366 
##        Turnaround Finger Roll Shot               Turnaround Hook Shot 
##                                  2                                  8 
##               Turnaround Jump Shot 
##                                891

Beaucoup d’actions sont répertoriées! Certaines n’ont même jamais été réalisé. Par exemple le ‘Turnaround Fadeaway Bank Jump Shot’ ou le ‘Cutting Finger Roll Layup Shot’.

summary(train$combined_shot_type)
## Bank Shot      Dunk Hook Shot Jump Shot     Layup  Tip Shot 
##       120      1056       127     19710      4532       152

Il existe beaucoup moins de type d’actions combinées. Kobe a réalisé peu de Hook Shot, c’est à dire un “bras roulé” ni même de Tip Shot qui sont des claquettes en français. On peut représenter le pourcentage de réussite suivant le type d’action :

pourcentage_par_action = prop.table(table(train$action_type, train$shot_made_flag),1)
pourcentage_par_action = as.data.frame.matrix(pourcentage_par_action)
pourcentage_par_action = pourcentage_par_action[order(pourcentage_par_action[,"1"]),]
pourcentage_par_action$action_type = rownames(pourcentage_par_action)
pourcentage_par_action$evaluation[pourcentage_par_action[, "1"] >= 0.9] = "90% >="
pourcentage_par_action$evaluation[pourcentage_par_action[, "1"] < 0.9] = "< 90%"

ggplot(data = pourcentage_par_action, aes(x = reorder(action_type, `1`))) +
  coord_flip() + 
  geom_bar(aes(y = `1`, fill= evaluation), stat = "identity") +
  scale_fill_manual(values = c("< 90%" = "darkblue", "90% >=" = "red"))
## Warning: Removed 2 rows containing missing values (position_stack).

Les actions rouges représentent celles pour lesquelles il y a plus de 90% de réussite. On réitère l’opération avec les actions combinées.

pourcentage_par_action = prop.table(table(train$combined_shot_type, train$shot_made_flag),1)
pourcentage_par_action = as.data.frame.matrix(pourcentage_par_action)
pourcentage_par_action = pourcentage_par_action[order(pourcentage_par_action[,"1"]),]
pourcentage_par_action$action_type = rownames(pourcentage_par_action)
pourcentage_par_action$evaluation[pourcentage_par_action[, "1"] >= 0.9] = "90% >="
pourcentage_par_action$evaluation[pourcentage_par_action[, "1"] < 0.9] = "< 90%"

ggplot(data = pourcentage_par_action, aes(x = reorder(action_type, `1`))) +
  coord_flip() + 
  geom_bar(aes(y = `1`, fill= evaluation), stat = "identity") +
  scale_fill_manual(values = c("< 90%" = "darkblue", "90% >=" = "red"))

L’angle de shot

En tant que joueur, je sais très bien que l’angle de tir est important. J’utilise particulièrement les shots à 45° avec la planche. On ne dispose malheureusement pas de la notion d’angle de tir dans notre jeu de données mais il est possible de le calculer. En effet, nous disposons des coordonnées cartésiennnes des tirs où l’origine de notre axe n’est autre que le panier.

On calcule un angle de tir allant de 0 à 360°. En effet, étant donné que notre origine est l’anneau, des tirs ont pu avoir lieu derrière le panier.

train$angle <- NA
train$angle = apply(train[,c('loc_x', 'loc_y')], 1, function(vector) {
  atan2(abs(vector[2]), abs(vector[1])) * 180 / pi
})

train$angle[train$loc_x < 0 & train$loc_y < 0] = 360 - train$angle[train$loc_x < 0 & train$loc_y < 0]
train$angle[train$loc_x > 0 & train$loc_y > 0] = 180 - train$angle[train$loc_x > 0 & train$loc_y > 0]
train$angle[train$loc_x > 0 & train$loc_y < 0] = 180 + train$angle[train$loc_x > 0 & train$loc_y < 0]

summary(train$angle)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    0.00   25.30   82.55   84.53  130.40  359.80
train$grouped_angle = cut(train$angle, breaks = seq(0, 360, 5), include.lowest=TRUE)

summary(train$grouped_angle)
##     [0,5]    (5,10]   (10,15]   (15,20]   (20,25]   (25,30]   (30,35] 
##      5069       334       318       339       344       398       447 
##   (35,40]   (40,45]   (45,50]   (50,55]   (55,60]   (60,65]   (65,70] 
##       530       648       749       778       734       591       472 
##   (70,75]   (75,80]   (80,85]   (85,90]   (90,95]  (95,100] (100,105] 
##       467       432       390       870       705       470       429 
## (105,110] (110,115] (115,120] (120,125] (125,130] (130,135] (135,140] 
##       504       666       808       869       847       825       653 
## (140,145] (145,150] (150,155] (155,160] (160,165] (165,170] (170,175] 
##       595       530       429       363       360       419       479 
## (175,180] (180,185] (185,190] (190,195] (195,200] (200,205] (205,210] 
##       554       466       171        52        20        11         9 
## (210,215] (215,220] (220,225] (225,230] (230,235] (235,240] (240,245] 
##         3         5         4         2         1         3         0 
## (245,250] (250,255] (255,260] (260,265] (265,270] (270,275] (275,280] 
##         1         2         5         3         0         0         2 
## (280,285] (285,290] (290,295] (295,300] (300,305] (305,310] (310,315] 
##         1         3         3         2         2         1         4 
## (315,320] (320,325] (325,330] (330,335] (335,340] (340,345] (345,350] 
##         3         1         5         2         9        11        51 
## (350,355] (355,360] 
##       142       282
courtplot(train$grouped_angle)
## Warning: Removed 105 rows containing missing values (geom_point).

pourcentage_par_angle = prop.table(table(train$grouped_angle, train$shot_made_flag),1)
pourcentage_par_angle = as.data.frame.matrix(pourcentage_par_angle)
pourcentage_par_angle = pourcentage_par_angle[order(pourcentage_par_angle[,"1"]),]
pourcentage_par_angle$grouped_angle = rownames(pourcentage_par_angle)
pourcentage_par_angle$evaluation[pourcentage_par_angle[, "1"] >= 0.9] = "90% >="
pourcentage_par_angle$evaluation[pourcentage_par_angle[, "1"] < 0.9] = "< 90%"

ggplot(data = pourcentage_par_angle, aes(x = reorder(grouped_angle, `1`))) +
  coord_flip() + 
  geom_bar(aes(y = `1`, fill= evaluation), stat = "identity") +
  scale_fill_manual(values = c("< 90%" = "darkblue", "90% >=" = "red"))
## Warning: Removed 3 rows containing missing values (position_stack).

Inspectons les shots qui ont un taux de réussite supérieur à 90%, c’est à dire pour un angle compris entre 225 et 325.

temporary_train = train[train$angle ==135,]
nrow(temporary_train)
## [1] 76

Ici, on calcule un angle que je nomme ‘absolue’, c’est à dire que l’on aura le même angle à gauche ou à droite du panier.

train$absolute_angle <- NA
train$absolute_angle = apply(train[,c('loc_x', 'loc_y')], 1, function(vector) {
  atan2(abs(vector[2]), abs(vector[1])) * 180 / pi
})

summary(train$absolute_angle)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   0.000   5.194  40.380  37.760  61.390  90.000
train$grouped_absolute_angle = cut(train$absolute_angle, breaks = seq(0, 90, 5), include.lowest=TRUE)

summary(train$grouped_absolute_angle)
##   [0,5]  (5,10] (10,15] (15,20] (20,25] (25,30] (30,35] (35,40] (40,45] 
##    6371    1126     840     730     727     838     985    1131    1388 
## (45,50] (50,55] (55,60] (60,65] (65,70] (70,75] (75,80] (80,85] (85,90] 
##    1500    1627    1608    1401    1142     976     867     865    1575
courtplot(train$grouped_absolute_angle)
## Warning: Removed 105 rows containing missing values (geom_point).

Cool! On a un jolie arc-en-ciel. Calculons maintenant les pourcentages de réussite aux tirs.

pourcentage_par_angle = prop.table(table(train$grouped_absolute_angle, train$shot_made_flag),1)
pourcentage_par_angle = as.data.frame.matrix(pourcentage_par_angle)
pourcentage_par_angle = pourcentage_par_angle[order(pourcentage_par_angle[,"1"]),]
pourcentage_par_angle$grouped_angle = rownames(pourcentage_par_angle)
pourcentage_par_angle$evaluation[pourcentage_par_angle[, "1"] >= 0.9] = "90% >="
pourcentage_par_angle$evaluation[pourcentage_par_angle[, "1"] < 0.9] = "< 90%"

ggplot(data = pourcentage_par_angle, aes(x = reorder(grouped_angle, `1`))) +
  coord_flip() + 
  geom_bar(aes(y = `1`, fill= evaluation), stat = "identity") +
  scale_fill_manual(values = c("< 90%" = "darkblue", "90% >=" = "red"))